# Copyright (c) 2016 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import print_function

"""VPP PCI Utility libraries"""

import re
import logging

from vpplib.VPPUtil import VPPUtil

DPDK_SCRIPT = "/vpp/vpp-config/scripts/dpdk-devbind.py"

# PCI Device id regular expresssion
PCI_DEV_ID_REGEX = "[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+.[0-9A-Fa-f]+"


class VppPCIUtil(object):
    """
    PCI Utilities

    """

    @staticmethod
    def _create_device_list(device_string):
        """
        Returns a list of PCI devices

        :param device_string: The devices string from dpdk_devbind
        :returns: The device list
        :rtype: dictionary
        """

        devices = {}

        ids = re.findall(PCI_DEV_ID_REGEX, device_string)
        descriptions = re.findall(r"\'([\s\S]*?)\'", device_string)
        unused = re.findall(r"unused=\w+|unused=", device_string)

        for i, j in enumerate(ids):
            device = {"description": descriptions[i]}
            if unused:
                device["unused"] = unused[i].split("=")[1].split(",")

            cmd = "ls /sys/bus/pci/devices/{}/driver/module/drivers".format(ids[i])
            (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
            if ret == 0:
                device["driver"] = stdout.split(":")[1].rstrip("\n")

            cmd = "cat /sys/bus/pci/devices/{}/numa_node".format(ids[i])
            (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
            if ret != 0:
                raise RuntimeError("{} failed {} {}".format(cmd, stderr, stdout))
            numa_node = stdout.rstrip("\n")
            if numa_node == "-1":
                device["numa_node"] = "0"
            else:
                device["numa_node"] = numa_node

            interfaces = []
            device["interfaces"] = []
            cmd = "ls /sys/bus/pci/devices/{}/net".format(ids[i])
            (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
            if ret == 0:
                interfaces = stdout.rstrip("\n").split()
                device["interfaces"] = interfaces

            l2_addrs = []
            for intf in interfaces:
                cmd = "cat /sys/bus/pci/devices/{}/net/{}/address".format(ids[i], intf)
                (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
                if ret != 0:
                    raise RuntimeError("{} failed {} {}".format(cmd, stderr, stdout))

                l2_addrs.append(stdout.rstrip("\n"))

            device["l2addr"] = l2_addrs

            devices[ids[i]] = device

        return devices

    def __init__(self, node):
        self._node = node
        self._dpdk_devices = {}
        self._kernel_devices = {}
        self._other_devices = {}
        self._crypto_dpdk_devices = {}
        self._crypto_kernel_devices = {}
        self._crypto_other_devices = {}
        self._link_up_devices = {}

    def get_all_devices(self):
        """
        Returns a list of all the devices

        """

        node = self._node
        rootdir = node["rootdir"]
        dpdk_script = rootdir + DPDK_SCRIPT
        cmd = dpdk_script + " --status"
        (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
        if ret != 0:
            raise RuntimeError(
                "{} failed on node {} {}".format(cmd, node["host"], stderr)
            )

        # Get the network devices using the DPDK
        # First get everything after using DPDK
        stda = stdout.split("Network devices using DPDK-compatible driver")[1]
        # Then get everything before using kernel driver
        using_dpdk = stda.split("Network devices using kernel driver")[0]
        self._dpdk_devices = self._create_device_list(using_dpdk)

        # Get the network devices using the kernel
        stda = stdout.split("Network devices using kernel driver")[1]
        using_kernel = stda.split("Other network devices")[0]
        self._kernel_devices = self._create_device_list(using_kernel)

        # Get the other network devices
        stda = stdout.split("Other network devices")[1]
        other = stda.split("Crypto devices using DPDK-compatible driver")[0]
        self._other_devices = self._create_device_list(other)

        # Get the crypto devices using the DPDK
        stda = stdout.split("Crypto devices using DPDK-compatible driver")[1]
        crypto_using_dpdk = stda.split("Crypto devices using kernel driver")[0]
        self._crypto_dpdk_devices = self._create_device_list(crypto_using_dpdk)

        # Get the network devices using the kernel
        stda = stdout.split("Crypto devices using kernel driver")[1]
        crypto_using_kernel = stda.split("Other crypto devices")[0]
        self._crypto_kernel_devices = self._create_device_list(crypto_using_kernel)

        # Get the other network devices
        crypto_other = stdout.split("Other crypto devices")[1]
        self._crypto_other_devices = self._create_device_list(crypto_other)

        # Get the devices used by the kernel
        for devk in self._kernel_devices.items():
            dvid = devk[0]
            device = devk[1]
            for i in device["interfaces"]:
                cmd = "ip addr show " + i
                (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
                if ret != 0:
                    raise RuntimeError(
                        "{} failed on node {} {}".format(cmd, node["host"], stderr)
                    )
                lstate = re.findall(r"state \w+", stdout)[0].split(" ")[1]

                # Take care of the links that are UP
                if lstate == "UP":
                    device["linkup"] = True
                    self._link_up_devices[dvid] = device

        for devl in self._link_up_devices.items():
            dvid = devl[0]
            del self._kernel_devices[dvid]

    def get_dpdk_devices(self):
        """
        Returns a list the dpdk devices

        """
        return self._dpdk_devices

    def get_kernel_devices(self):
        """
        Returns a list the kernel devices

        """
        return self._kernel_devices

    def get_other_devices(self):
        """
        Returns a list the other devices

        """
        return self._other_devices

    def get_crypto_dpdk_devices(self):
        """
        Returns a list the crypto dpdk devices

        """
        return self._crypto_dpdk_devices

    def get_crypto_kernel_devices(self):
        """
        Returns a list the crypto kernel devices

        """
        return self._crypto_kernel_devices

    def get_crypto_other_devices(self):
        """
        Returns a list the crypto other devices

        """
        return self._crypto_other_devices

    def get_link_up_devices(self):
        """
        Returns a list the link up devices

        """
        return self._link_up_devices

    @staticmethod
    def vpp_create_interface(interfaces, device_id, device):
        """
        Create an interface using the device is and device

        """

        name = "port" + str(len(interfaces))
        interfaces[name] = {}
        interfaces[name]["pci_address"] = device_id
        interfaces[name]["numa_node"] = device["numa_node"]
        if "l2addr" in device:
            l2_addrs = device["l2addr"]
            for i, j in enumerate(l2_addrs):
                if i > 0:
                    mname = "mac_address" + str(i + 1)
                    interfaces[name][mname] = l2_addrs[i]
                else:
                    interfaces[name]["mac_address"] = l2_addrs[i]

    @staticmethod
    def show_vpp_devices(devices, show_interfaces=True, show_header=True):
        """
        show the vpp devices specified in the argument

        :param devices: A list of devices
        :param show_interfaces: show the kernel information
        :param show_header: Display the header if true
        :type devices: dict
        :type show_interfaces: bool
        :type show_header: bool
        """

        if show_interfaces:
            header = "{:15} {:25} {:50}".format(
                "PCI ID", "Kernel Interface(s)", "Description"
            )
        else:
            header = "{:15} {:50}".format("PCI ID", "Description")
        dashseparator = "-" * (len(header) - 2)

        if show_header is True:
            print(header)
            print(dashseparator)
        for dit in devices.items():
            dvid = dit[0]
            device = dit[1]
            if show_interfaces:
                interfaces = device["interfaces"]
                interface = ""
                for i, j in enumerate(interfaces):
                    if i > 0:
                        interface += "," + interfaces[i]
                    else:
                        interface = interfaces[i]

                print(
                    "{:15} {:25} {:50}".format(dvid, interface, device["description"])
                )
            else:
                print("{:15} {:50}".format(dvid, device["description"]))

    @staticmethod
    def unbind_vpp_device(node, device_id):
        """
        unbind the device specified

        :param node: Node dictionary with cpuinfo.
        :param device_id: The device id
        :type node: dict
        :type device_id: string
        """

        rootdir = node["rootdir"]
        dpdk_script = rootdir + DPDK_SCRIPT
        cmd = dpdk_script + " -u " + " " + device_id
        (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
        if ret != 0:
            raise RuntimeError(
                "{} failed on node {} {} {}".format(cmd, node["host"], stdout, stderr)
            )

    @staticmethod
    def bind_vpp_device(node, driver, device_id):
        """
        bind the device specified

        :param node: Node dictionary with cpuinfo.
        :param driver: The driver
        :param device_id: The device id
        :type node: dict
        :type driver: string
        :type device_id: string
        :returns ret: Command return code
        """

        rootdir = node["rootdir"]
        dpdk_script = rootdir + DPDK_SCRIPT
        cmd = dpdk_script + " -b " + driver + " " + device_id
        (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
        if ret != 0:
            logging.error(
                "{} failed on node {}".format(cmd, node["host"], stdout, stderr)
            )
            logging.error("{} {}".format(stdout, stderr))

        return ret
